page.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. 'use client';
  2. import './style.scss';
  3. import 'animate.css';
  4. import { use, useEffect, useState } from 'react';
  5. import { useDonationAlert } from '@/hooks/useDonationAlert';
  6. import { DonationAlertConfig, DonationAlertData } from '@/types/donation';
  7. import { fetchApi } from '@/lib/utils/client';
  8. import View from './view';
  9. type Props = {
  10. params: Promise<{ widgetToken: string }>;
  11. searchParams: Promise<{ [key: string]: string|string[]|undefined }>;
  12. };
  13. const DEFAULT_CONFIG: DonationAlertConfig = {
  14. id: 0,
  15. title: '',
  16. amount: 0,
  17. matchType: 0,
  18. message: '',
  19. playDelaySec: 0,
  20. displayDurationSec: 10,
  21. popupEffect: null,
  22. textEffect: null,
  23. nicknameFontFamily: null,
  24. nicknameFontSize: 24,
  25. nicknameFontColor: '#FFD700',
  26. amountFontFamily: null,
  27. amountFontSize: 24,
  28. amountFontColor: '#FF6B35',
  29. messageFontFamily: null,
  30. messageFontSize: 18,
  31. messageFontColor: '#FFFFFF',
  32. templateFontFamily: null,
  33. templateFontSize: 24,
  34. templateFontColor: '#FFFFFF',
  35. enableImage: false,
  36. imageUrl: null,
  37. enableSound: false,
  38. soundUrl: null,
  39. isActive: true
  40. };
  41. function matchConfig(configs: DonationAlertConfig[], amount: number): DonationAlertConfig
  42. {
  43. // 1순위: Exact 매칭 (MatchType === 1)
  44. const exact = configs.find(c => c.matchType === 1 && c.amount === amount && c.isActive);
  45. if (exact) {
  46. return exact;
  47. }
  48. // 2순위: MinThreshold (MatchType === 0) — 금액 이상 중 가장 높은 것
  49. const thresholds = configs
  50. .filter(c => c.matchType === 0 && c.amount <= amount && c.isActive)
  51. .sort((a, b) => b.amount - a.amount);
  52. return thresholds[0] ?? DEFAULT_CONFIG;
  53. }
  54. export default function AlertPage({ params, searchParams }: Props)
  55. {
  56. const { widgetToken } = use(params);
  57. const sp = use(searchParams);
  58. const isPreview = sp.preview === '1';
  59. const hubUrl = process.env.NEXT_PUBLIC_API_URL + '/hubs/donation';
  60. const { current, remoteState, onAlertComplete } = useDonationAlert(widgetToken, hubUrl);
  61. const [configs, setConfigs] = useState<DonationAlertConfig[]>([]);
  62. const [previewConfig, setPreviewConfig] = useState<DonationAlertConfig|null>(null);
  63. // API에서 config 목록 로드
  64. useEffect(() => {
  65. fetchApi<{ list: DonationAlertConfig[] }>(`/api/widget/alert/config/${widgetToken}`, { silent: true }).then(res => {
  66. if (res.success && res.data?.list) {
  67. setConfigs(res.data.list);
  68. }
  69. }).catch(() => {});
  70. }, [widgetToken]);
  71. // postMessage 수신 (미리보기 모드)
  72. const [testAlert, setTestAlert] = useState<DonationAlertData|null>(null);
  73. useEffect(() => {
  74. if (!isPreview) {
  75. return;
  76. }
  77. const handler = (event: MessageEvent) => {
  78. if (event.origin !== window.location.origin) {
  79. return;
  80. }
  81. if (event.data?.type === 'ALERT_PREVIEW') {
  82. setPreviewConfig({
  83. ...DEFAULT_CONFIG,
  84. ...event.data.config,
  85. });
  86. }
  87. if (event.data?.type === 'ALERT_TEST') {
  88. setTestAlert({
  89. alertID: Date.now(),
  90. donationID: 0,
  91. correlationID: '',
  92. sponsorMemberID: 0,
  93. sendName: event.data.sendName || '테스트유저',
  94. amount: event.data.amount || 1000,
  95. netAmount: event.data.amount || 1000,
  96. message: event.data.message || null,
  97. channelID: 0,
  98. channelName: '',
  99. crewMemberID: null,
  100. crewMemberNickname: null,
  101. isTest: true,
  102. createdAt: new Date().toISOString()
  103. });
  104. }
  105. };
  106. window.addEventListener('message', handler);
  107. return () => window.removeEventListener('message', handler);
  108. }, [isPreview]);
  109. // 미리보기 테스트 알림 or 실제 알림
  110. const activeAlert = isPreview ? testAlert : current;
  111. const config = activeAlert ? (previewConfig ?? matchConfig(configs, activeAlert.amount)) : null;
  112. const handleComplete = () => {
  113. if (isPreview) {
  114. setTestAlert(null);
  115. } else {
  116. onAlertComplete();
  117. }
  118. };
  119. return (
  120. <div className="alert-page">
  121. {activeAlert && config && (
  122. <View
  123. key={activeAlert.alertID}
  124. alert={activeAlert}
  125. config={config}
  126. isAudioOnly={remoteState.isAudioOnly}
  127. isVideoOnly={remoteState.isVideoOnly}
  128. onComplete={handleComplete}
  129. />
  130. )}
  131. </div>
  132. );
  133. }